package org.obm.lmtp.impl;

import java.io.IOException;
import java.net.InetAddress;
import java.util.Date;

import org.obm.lmtp.backend.LmtpAddress;
import org.obm.lmtp.backend.LmtpEnvelope;
import org.obm.lmtp.backend.LmtpReply;
import org.obm.mina.Blob;
import org.obm.mina.ProtocolHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class LmtpHandler extends ProtocolHandler {
	// Connection specific data
	protected LmtpConfig config;
	protected LmtpWriter writer;
	protected String remoteAddress;
	protected String remoteHostname;
	private String lhloArg;

	// Message specific data
	protected LmtpEnvelope mEnvelope;

	private static final Logger logger = LoggerFactory
			.getLogger(LmtpHandler.class);

	LmtpHandler(MinaLmtpServer server) {
		config = server.getConfig();
	}

	protected boolean setupConnection(InetAddress remoteAddr) {
		remoteAddress = remoteAddr.getHostAddress();
		remoteHostname = remoteAddr.getHostName();
		if (remoteHostname == null || remoteHostname.length() == 0) {
			remoteHostname = remoteAddress;
		}
		sendReply(LmtpReply.GREETING);
		return true;
	}

	protected void notifyIdleConnection() {
		sendReply(LmtpReply.TIMEOUT);
	}

	protected boolean authenticate() {
		// LMTP doesn't need auth.
		return true;
	}

	protected boolean processCommand(String cmd) throws IOException {
		String arg = null;

		if (cmd == null) {
			logger.info("disconnected without quit");
			dropConnection();
			return false;
		}

		setIdle(false);

		int i = cmd.indexOf(' ');
		if (i > 0) {
			arg = cmd.substring(i + 1);
			cmd = cmd.substring(0, i);
		}

		logger.debug("command=" + cmd + " arg=" + arg);

		if (cmd.length() < 4) {
			doSyntaxError("command too short");
			return true;
		}

		int ch = cmd.charAt(0);

		// Breaking out of this switch causes a syntax error to be returned
		// So if you process a command then return immediately (even if the
		// command handler reported a syntax error or failed otherwise)

		switch (ch) {

		case 'l':
		case 'L':
			if ("LHLO".equalsIgnoreCase(cmd)) {
				doLHLO(arg);
				return true;
			}
			break;

		case 'm':
		case 'M':
			if ("MAIL".equalsIgnoreCase(cmd)) {
				final int fromColonLength = 5;
				if (arg.length() < fromColonLength) {
					break; // not enough room to carry "from:"
				}
				String fromColon = arg.substring(0, fromColonLength);
				if (!"FROM:".equalsIgnoreCase(fromColon)) {
					break; // there was no "from:"
				}
				arg = arg.substring(fromColonLength);
				doMAIL(arg);
				return true;
			}
			break;

		case 'r':
		case 'R':
			if ("RSET".equalsIgnoreCase(cmd)) {
				doRSET(arg);
				return true;
			}
			if ("RCPT".equalsIgnoreCase(cmd)) {
				final int toColonLength = 3;
				if (arg.length() < toColonLength) {
					break; // not enough room to carry "to:"
				}
				String toColon = arg.substring(0, toColonLength);
				if (!"TO:".equalsIgnoreCase(toColon)) {
					break; // there was no "to:"
				}
				arg = arg.substring(toColonLength);
				doRCPT(arg);
				return true;
			}
			break;

		case 'd':
		case 'D':
			if ("DATA".equalsIgnoreCase(cmd)) {
				doDATA(arg);
				return true;
			}
			break;

		case 'n':
		case 'N':
			if ("NOOP".equalsIgnoreCase(cmd)) {
				doNOOP();
				return true;
			}
			break;

		case 'q':
		case 'Q':
			if ("QUIT".equalsIgnoreCase(cmd)) {
				doQUIT();
				return false;
			}
			break;

		case 'v':
		case 'V':
			if ("VRFY".equalsIgnoreCase(cmd)) {
				doVRFY(arg);
				return true;
			}
			break;

		default:
			break;
		}
		doSyntaxError("unknown command");
		return true;
	}

	private void sendReply(LmtpReply reply) {
		writer.println(reply.toString());
		writer.flush();
	}

	private void doSyntaxError(String why) {
		sendReply(LmtpReply.SYNTAX_ERROR);
	}

	private void doNOOP() {
		sendReply(LmtpReply.OK);
	}

	private void doQUIT() {
		sendReply(LmtpReply.BYE);
		dropConnection();
	}

	private void doRSET(String arg) {
		if (arg != null) {
			doSyntaxError("parameter supplied to rset");
			return;
		}
		reset();
		sendReply(LmtpReply.OK);
	}

	private void doVRFY(String arg) {
		if (arg == null || arg.length() == 0) {
			doSyntaxError("no parameter to vrfy");
			return;
		}
		// RFC 2821, Section 7.3: If a site disables these commands for security
		// reasons, the SMTP server MUST return a 252 response, rather than a
		// code that could be confused with successful or unsuccessful
		// verification.
		// RFC 1892: X.3.3 System not capable of selected features
		sendReply(LmtpReply.USE_RCPT_INSTEAD);
	}

	private void doLHLO(String arg) {
		lhloArg = arg;
		if (arg == null || arg.length() == 0) {
			doSyntaxError("no parameter to lhlo");
			return;
		}
		writer.println("250-" + config.getName());
		writer.println("250-8BITMIME");
		writer.println("250-ENHANCEDSTATUSCODES");
		writer.println("250-SIZE ");
		writer.println("250 PIPELINING");
		writer.flush();
		reset();
	}

	private void doMAIL(String arg) {
		if (arg == null || arg.length() == 0) {
			doSyntaxError("no parameter to mail from");
			return;
		}

		if (mEnvelope.hasSender()) {
			sendReply(LmtpReply.NESTED_MAIL_COMMAND);
			return;
		}

		LmtpAddress addr = new LmtpAddress(arg,
				new String[] { "BODY", "SIZE" }, null);
		if (!addr.isValid()) {
			logger.info("invalid address: " + arg);
			sendReply(LmtpReply.INVALID_SENDER_ADDRESS);
			return;
		}

		LmtpBodyType type = null;
		String body = addr.getParameter("BODY");
		if (body != null) {
			type = LmtpBodyType.getInstance(body);
			if (type == null) {
				sendReply(LmtpReply.INVALID_BODY_PARAMETER);
				return;
			}
		}

		String sz = addr.getParameter("SIZE");
		if (sz != null) {
			try {
				Integer.parseInt(sz);
			} catch (NumberFormatException nfe) {
				sendReply(LmtpReply.INVALID_SIZE_PARAMETER);
				return;
			}
		}

		mEnvelope.setSender(addr);
		sendReply(LmtpReply.SENDER_OK);
	}

	private void doRCPT(String arg) {
		if (arg == null || arg.length() == 0) {
			doSyntaxError("no parameter to rcpt to");
			return;
		}

		if (!mEnvelope.hasSender()) {
			sendReply(LmtpReply.MISSING_MAIL_TO);
			return;
		}

		LmtpAddress addr = new LmtpAddress(arg, null, config
				.getRecipientDelimiter());
		if (!addr.isValid()) {
			sendReply(LmtpReply.INVALID_RECIPIENT_ADDRESS);
			return;
		}

		LmtpReply reply = config.getLmtpBackend().getAddressStatus(addr);
		if (reply.success()) {
			mEnvelope.addRecipient(addr);
		}
		sendReply(reply);
	}

	protected void reset() {
		// Reset must not change any earlier LHLO argument
		mEnvelope = new LmtpEnvelope();
	}

	private void doDATA(String arg) throws IOException {
		if (!mEnvelope.hasRecipients()) {
			sendReply(LmtpReply.NO_RECIPIENTS);
			return;
		}
		sendReply(LmtpReply.OK_TO_SEND_DATA);
		continueDATA();
	}

	protected abstract void continueDATA() throws IOException;

	protected void processMessageData(Blob blob) throws IOException {
		config.getLmtpBackend().deliver(mEnvelope, blob);
		finishMessageData(blob.getRawSize());
	}

	private void finishMessageData(long size) throws IOException {
		int numDelivered = 0;
		for (LmtpAddress recipient : mEnvelope.getRecipients()) {
			LmtpReply reply = recipient.getDeliveryStatus();
			sendReply(reply);
			if (reply.success()) {
				numDelivered++;
			}
		}
		reset();
	}

	/*
	 * Generates the <tt>Return-Path</tt> and <tt>Received</tt> headers for the
	 * current incoming message.
	 */
	protected String getAdditionalHeaders() {
		StringBuilder headers = new StringBuilder();

		// Assemble Return-Path header
		if (mEnvelope.hasSender()) {
			String sender = mEnvelope.getSender().getEmailAddress();
			if (sender != null && sender.trim().length() > 0) {
				headers.append(String.format("Return-Path: %s\r\n", sender));
			}
		}

		// Assemble Received header
		String localHostname = "unknown";
		String timestamp = new MailDateFormat().format(new Date());
		String name = "Received: ";
		String value = String.format(
				"from %s (LHLO %s) (%s) by %s with LMTP; %s", remoteHostname,
				lhloArg, remoteAddress, localHostname, timestamp);
		headers.append(name);
		headers.append(MimeUtility.fold(name.length(), value));
		headers.append("\r\n");

		return headers.toString();
	}
}
